<?php

namespace Simanx\Spes\Dto;

use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Contracts\Support\Jsonable;
use Illuminate\Support\Arr;
use Simanx\Spes\Attribute\ClassAttributeProxy;
use Simanx\Spes\Validation\Attributes\Validation;

/**
 * 数据传输对象基类
 * @package Simanx\Spes\Dto
 */
abstract class Dto implements Arrayable, \ArrayAccess, Jsonable, \JsonSerializable
{
    private array $attributes;

    public function __construct(array|Arrayable $attributes = [])
    {
        if (is_array($attributes)) {
            $this->attributes = $attributes;
        } else {
            $this->attributes = $attributes->toArray();
        }
    }

    public function setAttribute(string $key, $value)
    {
        $this->$key = $value;
        $this->attributes[$key] = $this->$key;
    }

    public function only()
    {
        $result = [];
        foreach (Arr::wrap(func_get_args()) as $key) {
            $result[$key] = $this->getAttribute($key);
        }

        return $result;
    }

    public function getAttribute(string $key, $default = null)
    {
        return $this->attributes[$key] ?? value($default);
    }

    public function getAttributes()
    {
        return $this->attributes;
    }

    public function all()
    {
        return $this->getAttributes();
    }

    public function hasAttribute($key): bool
    {
        return isset($this->getAttributes()[$key]);
    }

    public function validate()
    {
        app('validator')
            ->validate($this->toArray(), ...$this->rules());
    }

    private function rules()
    {
        $rules = [];
        $messages = [];
        $reflectionClass = new \ReflectionClass($this);
        $properties = $reflectionClass->getProperties(\ReflectionProperty::IS_PUBLIC);
        foreach ($properties as $property) {
            $propertyName = $property->getName();
            $propertyTypeName = $property->getType()->getName();
            if (is_subclass_of($propertyTypeName, self::class) && $propertyValue = $this->getAttribute($propertyName)) {
                [$propertyRules, $propertyMessages] = $propertyValue->rules();
                foreach ($propertyRules as $key => $propertyRule) {
                    $rules[$propertyName . '.' . $key] = $propertyRule;
                }

                foreach ($propertyMessages as $key => $propertyMessage) {
                    $messages[$propertyName . '.' . $key] = $propertyMessage;
                }
            }

            $validations = $property->getAttributes(Validation::class);
            foreach ($validations as $validationAttribute) {
                $rules[$propertyName] = $rules[$propertyName] ?? [];

                /** @var Validation $validation */
                $validation = $validationAttribute->newInstance();
                $ruleNames = explode('|', $validation->rule);
                foreach ($ruleNames as $ruleName) {
                    $rules[$propertyName][] = $ruleName;
                    if ($validateMessage = $validation->message) {
                        $messages[$propertyName . '.' . explode(':', $ruleName)[0]] = $validateMessage;
                    }
                }
            }
        }

        return [$rules, $messages];
    }

    public function __get(string $key)
    {
        return $this->getAttribute($key);
    }

    public function __set(string $key, $value): void
    {
        $this->setAttribute($key, $value);
    }

    public function toArray()
    {
        return $this->getAttributes();
    }

    public function jsonSerialize(): array
    {
        return $this->toArray();
    }

    public function toJson($options = 0)
    {
        return json_encode($this->toArray(), $options);
    }

    public function offsetSet($offset, $value)
    {
        $this->setAttribute($offset, $value);
    }

    public function offsetGet($offset)
    {
        return $this->getAttribute($offset);
    }

    public function offsetExists($offset)
    {
        return isset($this->getAttributes()[$offset]);
    }

    public function offsetUnset($offset)
    {
        unset($this->getAttributes()[$offset]);
    }
}
